package org.jetlinks.community.mstn.aop;

import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.hswebframework.ezorm.core.param.Term;
import org.hswebframework.utils.StringUtils;
import org.hswebframework.web.aop.MethodInterceptorHolder;
import org.hswebframework.web.api.crud.entity.QueryParamEntity;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.id.IDGenerator;
import org.hswebframework.web.logger.ReactiveLogger;
import org.hswebframework.web.logging.AccessLoggerInfo;
import org.hswebframework.web.logging.RequestInfo;
import org.hswebframework.web.logging.events.AccessLoggerAfterEvent;
import org.hswebframework.web.logging.events.AccessLoggerBeforeEvent;
import org.hswebframework.web.utils.ReactiveWebUtils;
import org.jetlinks.community.mstn.annotation.MstnAssetsController;
import org.jetlinks.community.mstn.events.MstnAssetsHandlerAfterEvent;
import org.jetlinks.community.mstn.events.MstnAssetsHandlerBeforeEvent;
import org.jetlinks.community.mstn.exception.MstnAssetsUnknownException;
import org.jetlinks.community.mstn.handler.*;
import org.jetlinks.community.mstn.logging.MstnAssetsRequestCtx;
import org.jetlinks.community.mstn.logging.MstnAssetsRequestInfo;
import org.jetlinks.community.mstn.term.MstnTenantAssetsTermBuilder;
import org.jetlinks.core.enums.ErrorCode;
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import reactor.util.context.ContextView;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
public class AopTenantAssetsControllerHandler extends StaticMethodMatcherPointcutAdvisor implements MethodInterceptor,WebFilter {


    @Autowired(
            required = false
    )
    private final List<MstnAssetsTypeHandler> assetsTypeHandlers = new ArrayList();
    private final Map<CacheKey, MstnAssetsHandler> handlerCache = new ConcurrentReferenceHashMap();

    private static final MstnAssetsDefine UNSUPPORASSETS = new MstnAssetsDefine();

    @Autowired
    private ApplicationEventPublisher eventPublisher;
    public AopTenantAssetsControllerHandler() {
        this.setAdvice(this);
    }


    @Nullable
    @Override
    public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable {
        MethodInterceptorHolder holder = MethodInterceptorHolder.create(invocation);
        //请求上下文
        MstnAssetsRequestCtx requestCtx = this.createRequestCtx(holder);
        MstnAssetsHandler handler = this.handlerCache
                .computeIfAbsent(new CacheKey(ClassUtils.getUserClass(holder.getTarget()), holder.getMethod()), this::createHandler
                );
                //结果
        Object response = invocation.proceed();
        if (response instanceof Mono) {
            return this.wrapMonoResponse((Mono)response,handler, requestCtx).contextWrite(Context.of(MstnAssetsRequestCtx.class, requestCtx));
        } else {
            return response instanceof Flux ?
                    this.wrapFluxResponse((Flux)response,handler, requestCtx).contextWrite(Context.of(MstnAssetsRequestCtx.class, requestCtx)) : response;
        }
    }

    /**
     * 创建请求上下文
     * @param holder
     * @return
     */
    public MstnAssetsRequestCtx createRequestCtx(MethodInterceptorHolder holder){
        MstnAssetsRequestCtx assetsRequestCtx = new MstnAssetsRequestCtx();
        assetsRequestCtx.setId(IDGenerator.MD5.generate());
        assetsRequestCtx.setRequestTime(System.currentTimeMillis());
        Map<String, Object> value = new ConcurrentHashMap();
        String[] names = holder.getArgumentsNames();
        Object[] args = holder.getArguments();

        for(int i = 0; i < args.length; ++i) {
            String name = names[i];
            Object val = args[i];
            if (val == null) {
                value.put(name, "null");
            } else if (val instanceof Mono) {
                args[i] = ((Mono)val).doOnNext((param) -> {
                    value.put(name, param);
                    if (assetsRequestCtx.getTerm() != null&& param instanceof  QueryParamEntity){
                        QueryParamEntity entity = (QueryParamEntity)param;
                        entity.getTerms().add(assetsRequestCtx.getTerm());
                    }
                });
            } else if (val instanceof Flux) {
                List<Object> arr = new ArrayList();
                value.put(name, arr);
                args[i] = ((Flux)val).doOnNext((param) -> {
                    arr.add(param);
                    if (param instanceof QueryParamEntity && assetsRequestCtx.getTerm() != null){
                        QueryParamEntity entity = (QueryParamEntity)param;
                        entity.getTerms().add(assetsRequestCtx.getTerm());
                    }
                });
            } else {
                value.put(name, val);
            }
        }

        assetsRequestCtx.setParameters(value);
        assetsRequestCtx.setTarget(holder.getTarget().getClass());
        assetsRequestCtx.setMethod(holder.getMethod());
        return assetsRequestCtx;
    }

    protected Mono<?> wrapMonoResponse(Mono<?> mono, MstnAssetsHandler handler, MstnAssetsRequestCtx requestCtx) {
        return this.wrapFluxResponse(mono.flux(), handler, requestCtx).singleOrEmpty();
    }

    protected Flux<?> wrapFluxResponse(Flux<?> flux, MstnAssetsHandler handler, MstnAssetsRequestCtx requestCtx) {
        return Flux.deferContextual((ctx) -> {
            Mono<MstnAssetsRequestInfo> assetsRequestInfo = this.currentRequestInfo(ctx);
            requestCtx.getClass();
            Flux var4 = assetsRequestInfo.doOnNext(requestCtx::putCtxInfo)
                    .then(this.beforeRequest(handler, requestCtx))
                    .thenMany(flux);
            requestCtx.getClass();
            return var4.doFinally((signal) -> {
                this.completeRequest(ctx, requestCtx);
            });
        });
    }

    private Mono<Void> beforeRequest(MstnAssetsHandler handler, MstnAssetsRequestCtx requestCtx) {
        MstnAssetsHandlerBeforeEvent event = new MstnAssetsHandlerBeforeEvent(requestCtx);

        return handler.beforeHandler(requestCtx)
            .then(event.publish(this.eventPublisher));
    }


    private void completeRequest( ContextView ctx, MstnAssetsRequestCtx requestCtx) {
        requestCtx.setResponseTime(System.currentTimeMillis());
        (new MstnAssetsHandlerAfterEvent(requestCtx)).publish(this.eventPublisher)
                .contextWrite(ctx).subscribe();
    }

    private Mono<MstnAssetsRequestInfo> currentRequestInfo(ContextView context) {
        if (context.hasKey(MstnAssetsRequestInfo.class)) {
            MstnAssetsRequestInfo info = (MstnAssetsRequestInfo)context.get(MstnAssetsRequestInfo.class);
            ReactiveLogger.log(context, (ctx) -> {
                info.setContext(new HashMap(ctx));
            });
            return Mono.just(info);
        } else {
            return Mono.empty();
        }
    }

    //TODO @link https://hanta.yuque.com/px7kg1/nn1gdr/hpiyu78tg78fe6x7
    private MstnAssetsHandler createHandler(CacheKey cacheKey) {
        MstnAssetsController methodAnn = AnnotationUtils.findAnnotation(cacheKey.method, MstnAssetsController.class);
        MstnAssetsController typeAnn = AnnotationUtils.findAnnotation(cacheKey.type, MstnAssetsController.class);
        //封装资产类型
        MstnAssetsDefine define = MstnAssetsDefine.of(methodAnn,typeAnn);
        if (StringUtils.isNullOrEmpty(define.type)){
            throw new MstnAssetsUnknownException(ErrorCode.UNKNOWN, "没有指定资产类型");
        }
        if (methodAnn == null && typeAnn == null) {
            //没有任何逻辑处理
            return new MstnAssetsEmptyHandler();
        } else {
            //根据方法和资产类型MstnAssetsDefine 创建处理器
            return createAssetsHandler(cacheKey.method, define);
        }
    }

    private MstnAssetsHandler createAssetsHandler(Method method, MstnAssetsDefine ann) {
        //是否返回mono
        boolean returnMono = method.getReturnType().isAssignableFrom(Mono.class);
        //是否返回flux
        boolean returnFlux = method.getReturnType().isAssignableFrom(Flux.class);
        //没有被流包裹
        if (!returnMono&&!returnFlux){
            //没有任何逻辑处理
            return new MstnAssetsEmptyHandler();
        }
        //方法没有参数
        if (method.getParameterTypes().length == 0) {
            //没有任何逻辑处理
            return new MstnAssetsEmptyHandler();
        }
        //没有忽略动态查询参数,则尝试获取参数中的动态查询条件,暂时只实现此方案
        if (!ann.ignoreQuery) {
            for (Class parameter : method.getParameterTypes()){
                if (parameter == QueryParamEntity.class) {
                    return new MstnAssetsQueryParamHandler(ann,returnMono,returnFlux);
                } else {
                    return new MstnAssetsMonoQueryParamHandler(ann,returnMono,returnFlux);
                }
            }
        }
        //方法参数中,资产ID对应的索引,从0开始. 通常用于 根据id进行某个操作时.比如:根据id查询,根据id修改
        int index = ann.assetIdIndex;
        if(index != -1) {
            //我不会用
//            ResolvableType type = ResolvableType.forMethodParameter(method,index);
            Class parameter = method.getParameterTypes()[index];
            //判断参数类型
            if ( parameter == Mono.class) {
                if (ann.autoBind) {
                    return new MstnAssetsMonoAutoBindHandler();
                } else {
                    return new MstnAssetsMonoAssetsHandler();
                }
            } else if(parameter == Flux.class){
                if (ann.autoBind) {
                    return new MstnAssetsFluxAutoBindHandler();
                } else {
                    return new MstnAssetsFluxAssetsHandler();
                }
            }else {
                //非响应式参数
                if (ann.autoBind) {

                    return new MstnAssetsAutoBindHandler(
//                            ann,returnMono,returnFlux,method.
                    );
                } else {
                    return new MstnAssetsUnAutoBindHandler();
                }
            }
        }

        return new MstnAssetsEmptyHandler();
    }

    private MstnAssetsDefine createDefineHandler(MethodInterceptorHolder holder) {
        return this.assetsTypeHandlers.stream().filter((parser) -> {
            return parser.support(ClassUtils.getUserClass(holder.getTarget()), holder.getMethod());
        }).findAny().map((parser) -> {
            return parser.parse(holder);
        }).orElse(UNSUPPORASSETS);
    }
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        MstnAssetsController assetsController = AnnotationUtils.findAnnotation(targetClass, MstnAssetsController.class);
        return assetsController != null && !assetsController.ignore() && this.assetsTypeHandlers.stream().anyMatch((handler) -> {
            return handler.support(targetClass,method);
        });
    }
    public int getOrder() {
        return Integer.MIN_VALUE;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(exchange).contextWrite(Context.of(MstnAssetsRequestInfo.class, this.createAccessInfo(exchange)));
    }

    private MstnAssetsRequestInfo createAccessInfo(ServerWebExchange exchange) {
        MstnAssetsRequestInfo info = new MstnAssetsRequestInfo();
        ServerHttpRequest request = exchange.getRequest();
        info.setRequestId(request.getId());
        info.setPath(request.getPath().value());
        info.setRequestMethod(request.getMethodValue());
        info.setHeaders(request.getHeaders().toSingleValueMap());
        Optional.ofNullable(ReactiveWebUtils.getIpAddr(request)).ifPresent(info::setIpAddr);
        return info;
    }



    private static class CacheKey {
        private Class<?> type;
        private Method method;

        public CacheKey(Class<?> type, Method method) {
            this.type = type;
            this.method = method;
        }

        public boolean equals(Object o) {
            if (o == this) {
                return true;
            } else if (!(o instanceof CacheKey)) {
                return false;
            } else {
                CacheKey other = (CacheKey)o;
                if (!other.canEqual(this)) {
                    return false;
                } else {
                    Object this$type = this.type;
                    Object other$type = other.type;
                    if (this$type == null) {
                        if (other$type != null) {
                            return false;
                        }
                    } else if (!this$type.equals(other$type)) {
                        return false;
                    }

                    Object this$method = this.method;
                    Object other$method = other.method;
                    if (this$method == null) {
                        if (other$method != null) {
                            return false;
                        }
                    } else if (!this$method.equals(other$method)) {
                        return false;
                    }

                    return true;
                }
            }
        }

        protected boolean canEqual(Object other) {
            return other instanceof CacheKey;
        }

        public int hashCode() {
            int result = 1;
            Object $type = this.type;
            result = result * 59 + ($type == null ? 43 : $type.hashCode());
            Object $method = this.method;
            result = result * 59 + ($method == null ? 43 : $method.hashCode());
            return result;
        }
    }
}
